refactor(blog): split AEO/GEO content into a new /library section#5516
Conversation
Blog was mixing hand-written editorial posts with listicle/comparison/how-to posts optimized for answer engines. Extract a generic content engine (lib/content) shared by both sections, move the 6 AEO/GEO posts into a new /library route tree, and keep /blog editorial-only. - lib/content: generic registry factory, MDX components, SEO builders - lib/blog + lib/library: thin per-section instantiations over the shared engine - app/(landing)/library: mirrors the blog route tree via shared Content*Page components - app/sitemap.ts, app/robots.ts, navbar: updated for the new section - next.config.ts: permanent redirects from moved /blog/<slug> URLs to /library/<slug>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Blog routes are slimmed down to data + metadata; layout, loading skeletons, and SEO wiring move into reusable Six posts live under Discovery updates: Library in the Resources nav, Reviewed by Cursor Bugbot for commit e32db6f. Configure here. |
Greptile SummaryThis PR refactors the blog content pipeline by extracting a generic shared
Confidence Score: 5/5Safe to merge — this is a structural refactor with no runtime logic changes; the shared component architecture is consistent, canonical URLs are correctly updated, and SEO redirects are in place. The shared registry factory, SEO builders, and Content*Page components are well-parameterized. MDX frontmatter canonical URLs were updated to /library/ paths, the 6 permanent redirects preserve existing indexed URLs, and sitemap.ts/robots.ts are correctly extended. The only noteworthy design wrinkle is that invalidateCaches() clears the module-level author cache shared between both registries, but the function has no active call site in the codebase, so this has no current effect. apps/sim/lib/content/registry-factory.ts — the invalidateCaches implementation has a subtle cross-registry side effect worth reviewing if cache invalidation is ever wired up. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
Factory[createContentRegistry] -->|blog instance| BR[blogRegistry]
Factory -->|library instance| LR[libraryRegistry]
BR -->|shared authorsDir| AC[(authorsCacheByDir module-level)]
LR -->|shared authorsDir| AC
BR --> BM[cachedMeta - blog]
LR --> LM[cachedMeta - library]
BR --> BlogIndex[blog index and slug pages]
BR --> BlogRSS[blog rss.xml]
LR --> LibIndex[library index and slug pages]
LR --> LibRSS[library rss.xml]
LR --> LibSitemap[library sitemap-images.xml]
BR --> Sitemap[sitemap.xml]
LR --> Sitemap
OldBlog[old blog slug URLs] -->|301 permanent redirect| LibSlug[library slug pages]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
Factory[createContentRegistry] -->|blog instance| BR[blogRegistry]
Factory -->|library instance| LR[libraryRegistry]
BR -->|shared authorsDir| AC[(authorsCacheByDir module-level)]
LR -->|shared authorsDir| AC
BR --> BM[cachedMeta - blog]
LR --> LM[cachedMeta - library]
BR --> BlogIndex[blog index and slug pages]
BR --> BlogRSS[blog rss.xml]
LR --> LibIndex[library index and slug pages]
LR --> LibRSS[library rss.xml]
LR --> LibSitemap[library sitemap-images.xml]
BR --> Sitemap[sitemap.xml]
LR --> Sitemap
OldBlog[old blog slug URLs] -->|301 permanent redirect| LibSlug[library slug pages]
Reviews (4): Last reviewed commit: "fix(blog): RSS lastBuildDate reflects th..." | Re-trigger Greptile |
Author pages for unknown/unmatched ids fell back to author?.id, which is
undefined when no post matches — collapsing canonical/OG URLs to
{basePath}/authors/ instead of {basePath}/authors/{id}. Pass the route id
explicitly so canonical always resolves correctly.
|
Fixed in ef7c0b1 — |
… cache, dead-export docs - RSS lastBuildDate now uses updated ?? date so edits after publication are reflected, matching the per-item pubDate semantics (blog + library) - Author JSON is now cached once per authorsDir at module scope instead of once per registry instantiation, since blog and library point at the same directory - Documented getNavPosts as reserved-but-unwired, matching the existing PLATFORM_MENU/SOLUTIONS_MENU convention in the navbar
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 91a7a3f. Configure here.
A featured post older than POSTS_PER_PAGE would appear both on page 1's featured row (sorted to the front) and again on its natural date-sorted page, since only page 1 excluded featured posts from the remaining list. Carve featured posts out of the paginated pool up front so pagination stays consistent and duplicate-free across all pages.
|
Addressed the pagination dedup bug Greptile flagged in its summary (confidence 4/5) — fixed in 43bc002. |
|
@cursor review |
…west item lastBuildDate only looked at items[0]'s updated/date, so revising an older post already in the feed wouldn't advance it. Extract the max-across-posts logic sitemap.ts already had into a shared lib/content/utils helper (latestModified) and use it in both RSS routes and the sitemap, so a revision to any feed item is reflected.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit e32db6f. Configure here.
…-*-page components The blog/library split (#5516) moved the blog post/index/author JSX into shared ContentPostPage/ContentIndexPage/ContentAuthorPage components while this branch was in flight, so the original unoptimized removal (verified local-only ogImage paths for both blog and library content) needs to land on those shared components instead of the old per-route JSX.
… pages (#5522) * fix(landing): fix Core Web Vitals regressions across public marketing pages - root layout unconditionally rendered next-runtime-env's PublicEnvScript, which calls unstable_noStore() and silently forced every route in the app dynamic - marketing pages never got static/ISR caching despite their own revalidate. Gated it to self-hosted only; hosted now uses a static, build-time equivalent (app/_shell/public-env-script.tsx) - removed real pointer-drag handlers from the hero's decorative workflow animation (was draggable despite being aria-hidden) - disabled dragging/panning on the (currently unmounted) landing-preview ReactFlow canvas so it's static-by-default if it's ever wired in - lazy-mount the Product Demo section's duplicate HeroVisual instance via next/dynamic + IntersectionObserver instead of loading it eagerly below the fold - disabled Next.js Link prefetch on always-in-viewport /signup and /login CTAs (navbar, hero, mobile nav) so their JS isn't fetched on every pageview regardless of whether the visitor clicks - removed `unoptimized` from local blog/integration images (including the priority LCP image on every blog post), letting next/image serve resized AVIF/WebP instead of full-size originals * fix(landing): address review findings on the CWV PR - revert prefetch={false} on below-fold CTAs (cta.tsx, enterprise.tsx) - contradicts the prefetch-on-approach rule this PR itself documents - restore unoptimized on avatarUrl and the MDX body-image renderer, both of which can legitimately hold external URLs outside next.config.ts's image remotePatterns allow-list - simplify handleAnchors to a single block argument now that positions are static (the second "live position" argument was always identical to the first after the drag-handler removal) - extract the near-duplicate IntersectionObserver lazy-mount logic shared by landing-preview-mount.tsx and product-demo-visual-mount.tsx into a single apps/sim/app/(landing)/hooks/use-lazy-mount.ts hook - import next-runtime-env's own exported PUBLIC_ENV_KEY constant instead of a hardcoded string literal, and match its case-insensitive NEXT_PUBLIC_ filter exactly, removing any drift risk between the two implementations - drop plain inline comments with no TSDoc home in favor of relying on the existing TSDoc/CLAUDE.md documentation * fix(landing): carry the unoptimized-image fix onto the shared content-*-page components The blog/library split (#5516) moved the blog post/index/author JSX into shared ContentPostPage/ContentIndexPage/ContentAuthorPage components while this branch was in flight, so the original unoptimized removal (verified local-only ogImage paths for both blog and library content) needs to land on those shared components instead of the old per-route JSX. * fix(landing): document ogImage's local-path expectation, fix CLAUDE.md structure doc - add a one-line comment on ContentFrontmatterSchema.ogImage documenting that it's rendered without unoptimized and expects a local path, matching the existing avatarUrl comment convention (a reviewer noted ogImage's schema is technically unconstrained and seo.ts has an http-prefix branch, though all current content is local) - add the new hooks/ folder to the (landing) CLAUDE.md structure diagram and name use-lazy-mount.ts directly in the lazy-mount rule * fix(landing): escape </script> breakout in the static public-env script Greptile P1: a NEXT_PUBLIC_* value containing "</script>" would close the inline script early and could inject markup/script into every hosted page. Escape "<" in the serialized JSON before interpolating it, matching the standard JSON-in-script-tag safeguard.
Summary
lib/content: registry factory, MDX components, SEO builders) instantiated separately forlib/blogandlib/librarybest-zapier-alternatives,ai-agents-vs-rpa,ai-agent-vs-chatbot,openai-vs-n8n-vs-sim,ai-agent-ideas,how-to-create-an-ai-agent) into a newcontent/library/tree/libraryas an exact route-tree mirror of/blog(index,[slug], tags, authors, rss.xml, sitemap-images.xml), rendered through sharedContent*Pagecomponents so both sections stay byte-identical in layoutapp/sitemap.ts,app/robots.ts, and the navbar Resources menu for the new section/blog/<slug>URLs to/library/<slug>to preserve existing SEO/indexingType of Change
Testing
bun run type-checkclean (only pre-existing unrelated@aws-sdk/client-textracterrors)bunx biome check .clean across full repoNEXT_PUBLIC_APP_URL, unrelated to this change)/blog/, zero dangling imports to deletedlib/blogfilesChecklist